﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

namespace Atomic
{
    internal class ConfigurationReader
    {
        private const string ProtocolsKey = "Protocols";
        private const string CertificatesKey = "Certificates";
        private const string CertificateKey = "Certificate";
        private const string SslProtocolsKey = "SslProtocols";
        private const string EndpointDefaultsKey = "EndpointDefaults";
        private const string EndpointsKey = "Endpoints";
        private const string UrlKey = "Url";
        private const string ClientCertificateModeKey = "ClientCertificateMode";
        private const string SniKey = "Sni";

        private readonly IConfiguration _configuration;

        private IDictionary<string, CertificateConfig> _certificates;
        private EndpointDefaults _endpointDefaults;
        private IEnumerable<EndpointConfig> _endpoints;

        public ConfigurationReader(IConfiguration configuration)
        {
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }

        public IDictionary<string, CertificateConfig> Certificates => _certificates ??= ReadCertificates();
        public EndpointDefaults EndpointDefaults => _endpointDefaults ??= ReadEndpointDefaults();
        public IEnumerable<EndpointConfig> Endpoints => _endpoints ??= ReadEndpoints();

        private IDictionary<string, CertificateConfig> ReadCertificates()
        {
            var certificates = new Dictionary<string, CertificateConfig>(0, StringComparer.OrdinalIgnoreCase);

            var certificatesConfig = _configuration.GetSection(CertificatesKey).GetChildren();
            foreach (var certificateConfig in certificatesConfig)
            {
                certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
            }

            return certificates;
        }

        // "EndpointDefaults": {
        //     "Protocols": "Http1AndHttp2",
        //     "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
        //     "ClientCertificateMode" : "NoCertificate"
        // }
        private EndpointDefaults ReadEndpointDefaults()
        {
            var configSection = _configuration.GetSection(EndpointDefaultsKey);
            return new EndpointDefaults
            {
                //Protocols = ParseProtocols(configSection[ProtocolsKey]),
                //SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
                //ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey]),
            };
        }

        private IEnumerable<EndpointConfig> ReadEndpoints()
        {
            var endpoints = new List<EndpointConfig>();

            var endpointsConfig = _configuration.GetSection(EndpointsKey).GetChildren();
            foreach (var endpointConfig in endpointsConfig)
            {
                // "EndpointName": {
                //     "Url": "https://*:5463",
                //     "Protocols": "Http1AndHttp2",
                //     "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
                //     "Certificate": {
                //         "Path": "testCert.pfx",
                //         "Password": "testPassword"
                //     },
                //     "ClientCertificateMode" : "NoCertificate",
                //     "Sni": {
                //         "a.example.org": {
                //             "Certificate": {
                //                 "Path": "testCertA.pfx",
                //                 "Password": "testPassword"
                //             }
                //         },
                //         "*.example.org": {
                //             "Protocols": "Http1",
                //         }
                //     }
                // }

                //var url = endpointConfig[UrlKey];
                //if (string.IsNullOrEmpty(url))
                //{
                //    throw new InvalidOperationException(CoreStrings.FormatEndpointMissingUrl(endpointConfig.Key));
                //}

                var endpoint = new EndpointConfig
                {
                    //Name = endpointConfig.Key,
                    //Url = url,
                    //Protocols = ParseProtocols(endpointConfig[ProtocolsKey]),
                    ConfigSection = endpointConfig,
                    Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
                    //SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey)),
                    //ClientCertificateMode = ParseClientCertificateMode(endpointConfig[ClientCertificateModeKey]),
                    //Sni = ReadSni(endpointConfig.GetSection(SniKey), endpointConfig.Key),
                };

                endpoints.Add(endpoint);
            }

            return endpoints;
        }

        //private static Dictionary<string, SniConfig> ReadSni(IConfigurationSection sniConfig, string endpointName)
        //{
        //    var sniDictionary = new Dictionary<string, SniConfig>(0, StringComparer.OrdinalIgnoreCase);

        //    foreach (var sniChild in sniConfig.GetChildren())
        //    {
        //        // "Sni": {
        //        //     "a.example.org": {
        //        //         "Protocols": "Http1",
        //        //         "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
        //        //         "Certificate": {
        //        //             "Path": "testCertA.pfx",
        //        //             "Password": "testPassword"
        //        //         },
        //        //         "ClientCertificateMode" : "NoCertificate"
        //        //     },
        //        //     "*.example.org": {
        //        //         "Certificate": {
        //        //             "Path": "testCertWildcard.pfx",
        //        //             "Password": "testPassword"
        //        //         }
        //        //     }
        //        //     // The following should work once https://github.com/dotnet/runtime/issues/40218 is resolved
        //        //     "*": {}
        //        // }

        //        if (string.IsNullOrEmpty(sniChild.Key))
        //        {
        //            throw new InvalidOperationException(CoreStrings.FormatSniNameCannotBeEmpty(endpointName));
        //        }

        //        var sni = new SniConfig
        //        {
        //            Certificate = new CertificateConfig(sniChild.GetSection(CertificateKey)),
        //            //Protocols = ParseProtocols(sniChild[ProtocolsKey]),
        //            //SslProtocols = ParseSslProcotols(sniChild.GetSection(SslProtocolsKey)),
        //            //ClientCertificateMode = ParseClientCertificateMode(sniChild[ClientCertificateModeKey])
        //        };

        //        sniDictionary.Add(sniChild.Key, sni);
        //    }

        //    return sniDictionary;
        //}


        //private static ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
        //{
        //    if (Enum.TryParse<ClientCertificateMode>(clientCertificateMode, ignoreCase: true, out var result))
        //    {
        //        return result;
        //    }

        //    return null;
        //}

        //private static HttpProtocols? ParseProtocols(string protocols)
        //{
        //    if (Enum.TryParse<HttpProtocols>(protocols, ignoreCase: true, out var result))
        //    {
        //        return result;
        //    }

        //    return null;
        //}

        //private static SslProtocols? ParseSslProcotols(IConfigurationSection sslProtocols)
        //{
        //    var stringProtocols = sslProtocols.Get<string[]>();

        //    return stringProtocols?.Aggregate(SslProtocols.None, (acc, current) =>
        //    {
        //        if (Enum.TryParse(current, ignoreCase: true, out SslProtocols parsed))
        //        {
        //            return acc | parsed;
        //        }

        //        return acc;
        //    });
        //}

        //internal static void ThrowIfContainsHttpsOnlyConfiguration(EndpointConfig endpoint)
        //{
        //    if (endpoint.Certificate.IsFileCert || endpoint.Certificate.IsStoreCert)
        //    {
        //        throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, CertificateKey));
        //    }

        //    if (endpoint.ClientCertificateMode.HasValue)
        //    {
        //        throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, ClientCertificateModeKey));
        //    }

        //    if (endpoint.SslProtocols.HasValue)
        //    {
        //        throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SslProtocolsKey));
        //    }

        //    if (endpoint.Sni.Count > 0)
        //    {
        //        throw new InvalidOperationException(CoreStrings.FormatEndpointHasUnusedHttpsConfig(endpoint.Name, SniKey));
        //    }
        //}
    }
    // "EndpointDefaults": {
    //     "Protocols": "Http1AndHttp2",
    //     "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
    //     "ClientCertificateMode" : "NoCertificate"
    // }
    internal class EndpointDefaults
    {
        //public HttpProtocols? Protocols { get; set; }
        //public SslProtocols? SslProtocols { get; set; }
        //public ClientCertificateMode? ClientCertificateMode { get; set; }
    }

    // "EndpointName": {
    //     "Url": "https://*:5463",
    //     "Protocols": "Http1AndHttp2",
    //     "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
    //     "Certificate": {
    //         "Path": "testCert.pfx",
    //         "Password": "testPassword"
    //     },
    //     "ClientCertificateMode" : "NoCertificate",
    //     "Sni": {
    //         "a.example.org": {
    //             "Certificate": {
    //                 "Path": "testCertA.pfx",
    //                 "Password": "testPasswordA"
    //             }
    //         },
    //         "*.example.org": {
    //             "Protocols": "Http1",
    //         }
    //     }
    // }
    internal class EndpointConfig
    {
        private IConfigurationSection _configSection;
        private ConfigSectionClone _configSectionClone;

        //public string Name { get; set; }
        //public string Url { get; set; }
        //public HttpProtocols? Protocols { get; set; }
        //public SslProtocols? SslProtocols { get; set; }
        public CertificateConfig Certificate { get; set; }
        //public ClientCertificateMode? ClientCertificateMode { get; set; }
        public Dictionary<string, SniConfig> Sni { get; set; }

        // Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
        // We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
        // EndpointConfig properties to their default values. If a default value changes, the properties would no longer be equal,
        // but the config sections could still be equal.
        public IConfigurationSection ConfigSection
        {
            get => _configSection;
            set
            {
                _configSection = value;
                // The IConfigrationSection will mutate, so we need to take a snapshot to compare against later and check for changes.
                _configSectionClone = new ConfigSectionClone(value);
            }
        }

        //public override bool Equals(object obj) =>
        //    obj is EndpointConfig other &&
        //    Name == other.Name &&
        //    Url == other.Url &&
        //    (Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
        //    (SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) &&
        //    Certificate == other.Certificate &&
        //    (ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) == (other.ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) &&
        //    CompareSniDictionaries(Sni, other.Sni) &&
        //    _configSectionClone == other._configSectionClone;

        //public override int GetHashCode() => HashCode.Combine(Name, Url,
        //    Protocols ?? ListenOptions.DefaultHttpProtocols, SslProtocols ?? System.Security.Authentication.SslProtocols.None,
        //    Certificate, ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate, Sni.Count, _configSectionClone);

        public static bool operator ==(EndpointConfig lhs, EndpointConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
        public static bool operator !=(EndpointConfig lhs, EndpointConfig rhs) => !(lhs == rhs);

        private static bool CompareSniDictionaries(Dictionary<string, SniConfig> lhs, Dictionary<string, SniConfig> rhs)
        {
            if (lhs.Count != rhs.Count)
            {
                return false;
            }

            foreach (var (lhsName, lhsSniConfig) in lhs)
            {
                if (!rhs.TryGetValue(lhsName, out var rhsSniConfig) || lhsSniConfig != rhsSniConfig)
                {
                    return false;
                }
            }

            return true;
        }
    }

    internal class SniConfig
    {
        //public HttpProtocols? Protocols { get; set; }
        //public SslProtocols? SslProtocols { get; set; }
        public CertificateConfig Certificate { get; set; }
        //public ClientCertificateMode? ClientCertificateMode { get; set; }

        //public override bool Equals(object obj) =>
        //    obj is SniConfig other &&
        //    (Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
        //    (SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) &&
        //    Certificate == other.Certificate &&
        //    (ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate) == (other.ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate);

        //public override int GetHashCode() => HashCode.Combine(
        //    Protocols ?? ListenOptions.DefaultHttpProtocols, SslProtocols ?? System.Security.Authentication.SslProtocols.None,
        //    Certificate, ClientCertificateMode ?? Https.ClientCertificateMode.NoCertificate);

        public static bool operator ==(SniConfig lhs, SniConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
        public static bool operator !=(SniConfig lhs, SniConfig rhs) => !(lhs == rhs);
    }

    // "CertificateName": {
    //     "Path": "testCert.pfx",
    //     "Password": "testPassword"
    // }
    internal class CertificateConfig
    {
        public CertificateConfig(IConfigurationSection configSection)
        {
            ConfigSection = configSection;

            // Bind explictly to preserve linkability
            Path = configSection[nameof(Path)];
            KeyPath = configSection[nameof(KeyPath)];
            Password = configSection[nameof(Password)];
            Subject = configSection[nameof(Subject)];
            Store = configSection[nameof(Store)];
            Location = configSection[nameof(Location)];

            if (bool.TryParse(configSection[nameof(AllowInvalid)], out var value))
            {
                AllowInvalid = value;
            }
        }

        // For testing
        internal CertificateConfig()
        {
        }

        public IConfigurationSection ConfigSection { get; }

        // File
        public bool IsFileCert => !string.IsNullOrEmpty(Path);

        public string Path { get; set; }

        public string KeyPath { get; set; }

        public string Password { get; set; }

        // Cert store

        public bool IsStoreCert => !string.IsNullOrEmpty(Subject);

        public string Subject { get; set; }

        public string Store { get; set; }

        public string Location { get; set; }

        public bool? AllowInvalid { get; set; }

        public override bool Equals(object obj) =>
            obj is CertificateConfig other &&
            Path == other.Path &&
            KeyPath == other.KeyPath &&
            Password == other.Password &&
            Subject == other.Subject &&
            Store == other.Store &&
            Location == other.Location &&
            (AllowInvalid ?? false) == (other.AllowInvalid ?? false);

        public override int GetHashCode() => HashCode.Combine(Path, KeyPath, Password, Subject, Store, Location, AllowInvalid ?? false);

        public static bool operator ==(CertificateConfig lhs, CertificateConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
        public static bool operator !=(CertificateConfig lhs, CertificateConfig rhs) => !(lhs == rhs);
    }

    internal class ConfigSectionClone
    {
        public ConfigSectionClone(IConfigurationSection configSection)
        {
            Value = configSection.Value;

            // GetChildren() should return an empty IEnumerable instead of null, but we guard against it since it's a public interface.
            var children = configSection.GetChildren() ?? Enumerable.Empty<IConfigurationSection>();
            Children = children.ToDictionary(child => child.Key, child => new ConfigSectionClone(child));
        }

        public string Value { get; }
        public Dictionary<string, ConfigSectionClone> Children { get; }

        public override bool Equals(object? obj)
        {
            if (!(obj is ConfigSectionClone other))
            {
                return false;
            }

            if (Value != other.Value || Children.Count != other.Children.Count)
            {
                return false;
            }

            foreach (var kvp in Children)
            {
                if (!other.Children.TryGetValue(kvp.Key, out var child))
                {
                    return false;
                }

                if (kvp.Value != child)
                {
                    return false;
                }
            }

            return true;
        }

        public override int GetHashCode() => HashCode.Combine(Value, Children.Count);

        public static bool operator ==(ConfigSectionClone lhs, ConfigSectionClone rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
        public static bool operator !=(ConfigSectionClone lhs, ConfigSectionClone rhs) => !(lhs == rhs);
    }
    ///// <summary>
    ///// Describes the client certificate requirements for a HTTPS connection.
    ///// </summary>
    //public enum ClientCertificateMode
    //{
    //    /// <summary>
    //    /// A client certificate is not required and will not be requested from clients.
    //    /// </summary>
    //    NoCertificate,

    //    /// <summary>
    //    /// A client certificate will be requested; however, authentication will not fail if a certificate is not provided by the client.
    //    /// </summary>
    //    AllowCertificate,

    //    /// <summary>
    //    /// A client certificate will be requested, and the client must provide a valid certificate for authentication to succeed.
    //    /// </summary>
    //    RequireCertificate
    //}
}
